﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Storage;

namespace BloxGin.Cameras
{
    public class FirstPersonCamera : GameComponent,ICamera
    {
        private const float _rotationSpeed = 0.05f;
        private float _leftRightRotation = 0f;
        private float _upDownRotation = 0f;
        private Vector3 _cameraFinalTarget;

        public FirstPersonCamera(Game game)
            : base(game)
        {

        }

        public BoundingFrustum BoundingFrustum { get; protected set; }
        /// <summary>
        /// Gets camera view matrix.
        /// </summary>
        public Matrix View { get; protected set; }

        public Matrix InvertView { get { return Matrix.Invert(View); } }
        /// <summary>
        /// Gets or sets camera projection matrix.
        /// </summary>
        public Matrix Projection { get; protected set; }
        /// <summary>
        /// Gets camera view matrix multiplied by projection matrix.
        /// </summary>
        public Matrix ViewProjection { get { return View * Projection; } }

        public Matrix InvertViewProjection { get { return Matrix.Invert(View*Projection); } }

        // Are we shaking?
        private bool shaking;

        // The maximum magnitude of our shake offset
        private float shakeMagnitude;

        // The total duration of the current shake
        private float shakeDuration;

        // A timer that determines how far into our shake we are
        private float shakeTimer;

        // The shake offset vector
        private Vector3 shakeOffset;

        Random random = new Random();


        /// <summary>
        /// Gets or sets camera position.
        /// </summary>
        public Vector3 Position { get; set; }

        /// <summary>
        /// Gets or sets camera field of view.
        /// </summary>
        public float FieldOfView { get; protected set; }
        /// <summary>
        /// Gets or sets camera aspect ratio.
        /// </summary>
        public float AspectRatio { get; protected set; } 
        /// <summary>
        /// Gets or sets camera near plane distance.
        /// </summary>
        public float NearPlaneDistance { get; set; }
        /// <summary>
        /// Gets or sets camera far plane distance.
        /// </summary>
        public float FarPlaneDistance { get; set; }

        public Vector3 Direction { get; protected set; }

        public Matrix cameraRotation { get; protected set; }

        private Vector2 angles = Vector2.Zero;


        public override void Initialize()
        {
            NearPlaneDistance = 0.1f;
            FarPlaneDistance = 1000f;
            _upDownRotation = 0;
            _leftRightRotation = 0;
            CalculateProjection();
            base.Initialize();
        }

        public override void Update(GameTime gameTime)
        {
            // If we're shaking...
            if (shaking)
            {
                // Move our timer ahead based on the elapsed time
                shakeTimer += (float)gameTime.ElapsedGameTime.TotalSeconds;

                // If we're at the max duration, we're not going to be shaking anymore
                if (shakeTimer >= shakeDuration)
                {
                    shaking = false;
                    shakeTimer = shakeDuration;
                }

                // Compute our progress in a [0, 1] range
                float progress = shakeTimer / shakeDuration;

                // Compute our magnitude based on our maximum value and our progress. This causes
                // the shake to reduce in magnitude as time moves on, giving us a smooth transition
                // back to being stationary. We use progress * progress to have a non-linear fall 
                // off of our magnitude. We could switch that with just progress if we want a linear 
                // fall off.
                float magnitude = shakeMagnitude * (1f - (progress * progress));

                // Generate a new offset vector with three random values and our magnitude
                shakeOffset = new Vector3(NextFloat(), NextFloat(), NextFloat()) * magnitude;
            }

            CalculateView();
            ProcessInput((float)gameTime.ElapsedGameTime.TotalSeconds * 50f);
            base.Update(gameTime);
        }

                /// <summary>
        /// Helper to generate a random float in the range of [-1, 1].
        /// </summary>
        private float NextFloat()
        {
            return (float)random.NextDouble() * 2f - 1f;
        }
    

        public Vector3 Target
        {
            get { return _cameraFinalTarget; }
        }

        public float LeftRightRotation
        {
            get { return _leftRightRotation; }
            set
            {
                _leftRightRotation = value;
                CalculateView();
            }
        }

        public float UpDownRotation
        {
            get { return _upDownRotation; }
            set
            {
                _upDownRotation = value;
                CalculateView();
            }
        }

        MouseState prevMouseState;

        protected void CalculateView()
        {

            Matrix rotationMatrix = Matrix.CreateRotationX(angles.X) * Matrix.CreateRotationY(angles.Y);
            Vector3 cameraRotatedTarget = Vector3.Transform(Vector3.Forward, rotationMatrix);
            _cameraFinalTarget = Position + cameraRotatedTarget;
            Vector3 cameraRotatedUpVector = Vector3.Transform(Vector3.Up, rotationMatrix);
            cameraRotation = rotationMatrix;

            if (shaking)
            {
                Position += shakeOffset;
                _cameraFinalTarget += shakeOffset;
            }

            View = Matrix.CreateLookAt(Position, _cameraFinalTarget, cameraRotatedUpVector);

            BoundingFrustum = new BoundingFrustum(View * Projection);

            
        }

        /// <summary>
        /// Shakes the camera with a specific magnitude and duration.
        /// </summary>
        /// <param name="magnitude">The largest magnitude to apply to the shake.</param>
        /// <param name="duration">The length of time (in seconds) for which the shake should occur.</param>
        public void Shake(float magnitude, float duration)
        {
            // We're now shaking
            shaking = true;

            // Store our magnitude and duration
            shakeMagnitude = magnitude;
            shakeDuration = duration;

            // Reset our timer
            shakeTimer = 0f;
        }

        private void ProcessInput(float amountOfMovement)
        {
          
           
            int widthOver2;
            int heightOver2;
            widthOver2 = Game.Window.ClientBounds.Width / 2;
            heightOver2 = Game.Window.ClientBounds.Height / 2;


            MouseState currentMouseState = Mouse.GetState();

            // if (currentMouseState.RightButton == ButtonState.Pressed && prevMouseState.RightButton == ButtonState.Released)
            {
                Mouse.SetPosition(widthOver2, heightOver2);
            }
            // else if (currentMouseState.RightButton == ButtonState.Pressed)
            {
                if (currentMouseState.X != widthOver2)
                    angles.Y -= amountOfMovement / 80.0f * (currentMouseState.X - widthOver2);
                if (currentMouseState.Y != heightOver2)
                    angles.X -= amountOfMovement / 80.0f * (currentMouseState.Y - heightOver2);

                if (angles.X > 1.4) angles.X = 1.4f;
                if (angles.X < -1.4) angles.X = -1.4f;
                if (angles.Y > Math.PI) angles.Y -= 2 * (float)Math.PI;
                if (angles.Y < -Math.PI) angles.Y += 2 * (float)Math.PI;

                Mouse.SetPosition(widthOver2, heightOver2);
            }

            prevMouseState = currentMouseState;
        }

        protected virtual void CalculateProjection()
        {
            Projection = Matrix.CreatePerspectiveFieldOfView(MathHelper.PiOver4, Game.GraphicsDevice.Viewport.AspectRatio, NearPlaneDistance, FarPlaneDistance);
        }

        public void LookAt(Vector3 target)
        {
            // Doesn't take into account the rotated UP vector
            // Should calculate rotations here!
            View = Matrix.CreateLookAt(Position, target, Vector3.Up);
        }
    }
}
